/*
* Copyright [2014] [Christian Loehnert, krampenschiesser@gmail.com]
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package de.ks.text;
import com.google.common.base.Charsets;
import com.google.common.eventbus.Subscribe;
import com.google.common.io.Files;
import de.ks.activity.ActivityController;
import de.ks.activity.ActivityLoadFinishedEvent;
import de.ks.activity.initialization.ActivityCallback;
import de.ks.activity.initialization.ActivityInitialization;
import de.ks.activity.initialization.DatasourceCallback;
import de.ks.application.fxml.DefaultLoader;
import de.ks.eventsystem.bus.HandlingThread;
import de.ks.eventsystem.bus.Threading;
import de.ks.executor.group.LastExecutionGroup;
import de.ks.i18n.Localized;
import de.ks.javafx.FxCss;
import de.ks.javafx.ScreenResolver;
import de.ks.text.command.AsciiDocEditorCommand;
import de.ks.text.view.AsciiDocContent;
import de.ks.text.view.AsciiDocViewer;
import javafx.beans.property.SimpleStringProperty;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.geometry.Rectangle2D;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.*;
import javafx.scene.control.TextArea;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyCode;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.RowConstraints;
import javafx.scene.layout.StackPane;
import javafx.scene.web.WebView;
import javafx.stage.FileChooser;
import javafx.stage.Modality;
import javafx.stage.Stage;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.enterprise.inject.Instance;
import javax.enterprise.inject.spi.CDI;
import javax.inject.Inject;
import java.awt.*;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.net.URL;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;
public class AsciiDocEditor implements Initializable, DatasourceCallback<Object>, ActivityCallback {
public static CompletableFuture<DefaultLoader<Node, AsciiDocEditor>> load(Consumer<StackPane> viewConsumer, Consumer<AsciiDocEditor> controllerConsumer) {
ActivityInitialization initialization = CDI.current().select(ActivityInitialization.class).get();
return initialization.loadAdditionalControllerWithFuture(AsciiDocEditor.class)//
.thenApply(loader -> {
viewConsumer.accept((StackPane) loader.getView());
controllerConsumer.accept(loader.getController());
return loader;
});
}
private static final Logger log = LoggerFactory.getLogger(AsciiDocEditor.class);
@Inject
@FxCss
Instance<String> stylesheets;
@Inject
AsciiDocParser parser;
@Inject
ActivityController controller;
@Inject
ActivityInitialization initialization;
@Inject
Instance<AsciiDocEditorCommand> editorCommands;
@Inject
@FxCss
Instance<String> cssSheets;
@FXML
protected TextArea editor;
@FXML
protected Tab previewTab;
@FXML
protected Button help;
@FXML
protected Button saveToFileButton;
@FXML
protected GridPane mainPane;
@FXML
protected TabPane tabPane;
@FXML
protected StackPane root;
@FXML
protected HBox editorCommandPane;
@FXML
protected StackPane editorContainer;
@FXML
protected TextField searchField;
@FXML
protected TextArea plainHtml;
protected File lastFile;
protected final SimpleStringProperty text = new SimpleStringProperty();
protected LastExecutionGroup<String> renderGroup;
protected Button insertImage = null;
protected boolean focusOnEditor = true;
protected final Map<Class<?>, AsciiDocEditorCommand> commands = new HashMap<>();
protected Stage previewPopupStage;
protected volatile PersistentStoreBack persistentStoreBack;
protected volatile LastSearch lastSearch = null;
protected AsciiDocViewer preview;
protected AsciiDocViewer popupPreview;
protected Node previewNode;
protected Node popupPreviewNode;
@Override
public void initialize(URL location, ResourceBundle resources) {
initializePreview();
initializePopupPreview();
renderGroup = new LastExecutionGroup<>("adocrender", 500, controller.getExecutorService());
text.bindBidirectional(editor.textProperty());
editor.textProperty().addListener((p, o, n) -> {
if (n != null) {
renderGroup.schedule(() -> n)//
.thenApplyAsync(s -> {
storeBack(s);
return s;
}, controller.getExecutorService())//
.thenAcceptAsync(s -> {
if (previewTab.isSelected()) {
preview.clear();
preview.showDirect(s);
} else {
preview.preload(Collections.singletonList(new AsciiDocContent(AsciiDocViewer.DEFAULT, s)));
}
if (previewPopupStage != null) {
popupPreview.clear();
popupPreview.showDirect(s);
}
}, controller.getJavaFXExecutor());
}
});
tabPane.focusedProperty().addListener((p, o, n) -> {
if (n) {
if (tabPane.getSelectionModel().getSelectedIndex() == 0) {
if (focusOnEditor) {
editor.requestFocus();
}
}
} else {
focusOnEditor = true;
}
});
tabPane.getSelectionModel().selectedIndexProperty().addListener((p, o, n) -> {
if (n != null && n.intValue() == 1) {
controller.getJavaFXExecutor().submit(() -> preview.requestFocus());
preview.show(new AsciiDocContent(AsciiDocViewer.DEFAULT, editor.getText()));
}
if (o == null || n == null) {
return;
}
if (o.intValue() != 0 && n.intValue() == 0) {
controller.getJavaFXExecutor().submit(() -> editor.requestFocus());
}
});
addCommands();
editor.setOnKeyPressed(e -> {
KeyCode code = e.getCode();
if (code == KeyCode.S && e.isControlDown()) {
saveToFile();
e.consume();
}
if (e.getCode() == KeyCode.P && e.isControlDown()) {
showPreviewPopup();
e.consume();
}
if (e.getCode() == KeyCode.F && e.isControlDown()) {
showSearchField();
e.consume();
}
});
searchField.setVisible(false);
searchField.setOnKeyPressed(e -> {
if (e.getCode() == KeyCode.ENTER) {
searchForText();
e.consume();
}
});
searchField.setOnKeyReleased(e -> {
if (e.getCode() == KeyCode.ESCAPE) {
if (searchField.textProperty().getValueSafe().trim().isEmpty()) {
searchField.setVisible(false);
editor.requestFocus();
} else {
searchField.setText("");
}
}
});
}
protected void initializePreview() {
DefaultLoader<Node, AsciiDocViewer> previewLoader = initialization.loadAdditionalController(AsciiDocViewer.class);
previewNode = previewLoader.getView();
previewTab.setContent(previewNode);
preview = previewLoader.getController();
plainHtml.textProperty().bind(preview.currentHtmlProperty());
}
protected void initializePopupPreview() {
DefaultLoader<Node, AsciiDocViewer> previewLoader = initialization.loadAdditionalController(AsciiDocViewer.class);
popupPreviewNode = previewLoader.getView();
popupPreview = previewLoader.getController();
}
protected void searchForText() {
String searchKey = searchField.textProperty().getValueSafe().toLowerCase(Locale.ROOT);
String editorContent = editor.textProperty().getValueSafe().toLowerCase(Locale.ROOT);
searchField.setDisable(true);
CompletableFuture<Integer> search = CompletableFuture.supplyAsync(() -> {
if (lastSearch != null && lastSearch.matches(searchKey, editorContent)) {
int startPoint = lastSearch.getPosition() + searchKey.length();
int newPosition;
if (startPoint >= editorContent.length()) {
newPosition = -1;
} else {
newPosition = editorContent.substring(startPoint).indexOf(searchKey);
if (newPosition >= 0) {
newPosition += lastSearch.getPosition() + searchKey.length();
}
}
if (newPosition == -1) {
newPosition = editorContent.indexOf(searchKey);
}
lastSearch.setPosition(newPosition);
return newPosition;
} else {
int newPosition = editorContent.indexOf(searchKey);
lastSearch = new LastSearch(searchKey, editorContent).setPosition(newPosition);
return newPosition;
}
}, controller.getExecutorService());
search.thenAcceptAsync(index -> {
searchField.setDisable(false);
if (index >= 0) {
editor.positionCaret(index);
editor.requestFocus();
}
}, controller.getJavaFXExecutor());
}
protected void showSearchField() {
searchField.setVisible(true);
searchField.requestFocus();
}
protected void storeBack(String storeBack) {
if (this.persistentStoreBack != null) {
this.persistentStoreBack.save(storeBack);
}
}
private void addCommands() {
editorCommands.forEach(c -> {
Button button = new Button();
button.setMnemonicParsing(true);
button.focusedProperty().addListener((p, o, n) -> {
if (n) {
focusOnEditor = false;
}
});
c.initialize(this, button);
this.commands.put(c.getClass(), c);
button.setText(Localized.get(c.getName()));
button.setOnAction(e -> c.execute(editor));
editorCommandPane.getChildren().add(button);
});
}
@FXML
void saveToFile() {
FileChooser fileChooser = new FileChooser();
FileChooser.ExtensionFilter htmlFilter = new FileChooser.ExtensionFilter("html", ".html");
FileChooser.ExtensionFilter adocFilter = new FileChooser.ExtensionFilter("adoc", ".adoc");
FileChooser.ExtensionFilter pdfFilter = new FileChooser.ExtensionFilter("pdf", ".pdf");
fileChooser.getExtensionFilters().add(htmlFilter);
fileChooser.getExtensionFilters().add(adocFilter);
fileChooser.getExtensionFilters().add(pdfFilter);
if (lastFile != null) {
fileChooser.setInitialDirectory(lastFile.getParentFile());
fileChooser.setInitialFileName(lastFile.getName());
if (lastFile.getName().endsWith(".html")) {
fileChooser.setSelectedExtensionFilter(htmlFilter);
} else if (lastFile.getName().endsWith(".pdf")) {
fileChooser.setSelectedExtensionFilter(pdfFilter);
} else {
fileChooser.setSelectedExtensionFilter(adocFilter);
}
} else {
fileChooser.setInitialFileName("export.html");
}
File file = fileChooser.showSaveDialog(saveToFileButton.getScene().getWindow());
if (file == null) {
return;
}
this.lastFile = file;
String extension = fileChooser.getSelectedExtensionFilter().getExtensions().get(0);
if (!file.getName().endsWith(extension)) {
file = new File(file.getPath() + extension);
}
if (extension.endsWith("adoc")) {
try {
Files.write(editor.getText(), file, Charsets.UTF_8);
} catch (IOException e) {
log.error("Could not write file {}", file, e);
}
} else if (extension.endsWith("pdf")) {
this.parser.renderToFile(editor.getText(), AsciiDocBackend.PDF, file);
} else {
this.parser.renderToFile(editor.getText(), AsciiDocBackend.HTML5, file);
}
}
@FXML
void showHelp() {
String title = Localized.get("help");
title = StringUtils.remove(title, "_");
new Thread(() -> {
Desktop desktop = Desktop.getDesktop();
URI uri = URI.create("http://powerman.name/doc/asciidoc");
try {
desktop.browse(uri);
} catch (IOException e) {
log.error("Could not browse {}", uri, e);
}
}).start();
}
@FXML
void showPreviewPopup() {
if (previewPopupStage == null) {
String title = Localized.get("adoc.preview");
previewPopupStage = new Stage();
previewPopupStage.setTitle(title);
Scene scene = new Scene(new StackPane(popupPreviewNode));
scene.setOnKeyReleased(e -> {
if (e.getCode() == KeyCode.ESCAPE) {
previewPopupStage.close();
}
});
previewPopupStage.setScene(scene);
Rectangle2D bounds = new ScreenResolver().getScreenToShow().getBounds();
previewPopupStage.setX(bounds.getMinX());
previewPopupStage.setY(bounds.getMinY());
previewPopupStage.setWidth(bounds.getWidth());
previewPopupStage.setHeight(bounds.getHeight());
previewPopupStage.initModality(Modality.NONE);
previewPopupStage.setOnShowing(e -> {
popupPreview.showDirect(getText());
});
previewPopupStage.setOnCloseRequest(e -> this.previewPopupStage = null);
previewPopupStage.show();
}
}
public SimpleStringProperty textProperty() {
return text;
}
public String getText() {
return text.getValueSafe();
}
public void setText(String text) {
this.editor.setText(text);
}
public void hideActionBar() {
RowConstraints rowConstraints = mainPane.getRowConstraints().get(1);
rowConstraints.setMinHeight(0.0);
rowConstraints.setPrefHeight(0.0);
rowConstraints.setMaxHeight(0.0);
}
public TextArea getEditor() {
return editor;
}
@SuppressWarnings("unchecked")
public <T extends AsciiDocEditorCommand> T getCommand(Class<T> clazz) {
return (T) commands.get(clazz);
}
public void selectPreview() {
this.tabPane.getSelectionModel().select(1);
}
public void selectEditor() {
this.tabPane.getSelectionModel().select(0);
}
public WebView getPreview() {
return preview.getWebView();
}
public AsciiDocEditor setPersistentStoreBack(String id, File dir) {
this.persistentStoreBack = new PersistentStoreBack(id, dir);
return this;
}
public boolean removePersistentStoreBack() {
if (persistentStoreBack != null) {
persistentStoreBack = null;
return true;
} else {
return false;
}
}
@Subscribe
@Threading(HandlingThread.JavaFX)
public void onRefresh(ActivityLoadFinishedEvent e) {
controller.getJavaFXExecutor().submit(() -> {
if (editor.textProperty().getValueSafe().trim().isEmpty()) {
if (persistentStoreBack != null) {
String text = persistentStoreBack.load();
editor.setText(text);
}
}
});
}
@Override
public void duringLoad(Object model) {
//nope
}
@Override
public void duringSave(Object model) {
if (persistentStoreBack != null) {
persistentStoreBack.delete();
}
}
@Override
public void onSuspend() {
controller.getJavaFXExecutor().invokeInJavaFXThread(() -> {
if (previewPopupStage != null) {
previewPopupStage.close();
}
return null;
});
}
@Override
public void onStop() {
onSuspend();
}
public PersistentStoreBack getPersistentStoreBack() {
return persistentStoreBack;
}
}